/*
 * Copyright (C) 2011 The original author or authors.
 * 
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
 * in compliance with the License. You may obtain a copy of the License at
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software distributed under the License
 * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
 * or implied. See the License for the specific language governing permissions and limitations under
 * the License.
 */

package com.zapta.apps.maniana.sync;

import android.os.AsyncTask;
import android.util.Log;
import android.util.Pair;

import com.zapta.apps.maniana.main.MainActivityState;
import com.zapta.apps.maniana.model.AppModel;
import com.zapta.apps.maniana.model.IDSetModel;
import com.zapta.apps.maniana.model.ItemColor;
import com.zapta.apps.maniana.model.ItemModel;
import com.zapta.apps.maniana.model.PageKind;

import java.io.IOException;
import java.util.Date;
import java.util.Properties;

import javax.mail.Flags;
import javax.mail.Flags.Flag;
import javax.mail.Folder;
import javax.mail.Message;
import javax.mail.MessagingException;
import javax.mail.NoSuchProviderException;
import javax.mail.Session;
import javax.mail.Store;
import javax.mail.internet.MimeMessage;


class ManianaMessage extends MimeMessage {
	
	String mMessageID;
	
	ManianaMessage(Session session, String messageID) {
		super(session);
		mMessageID = messageID;
	}
	
	ManianaMessage(MimeMessage source) throws MessagingException {
		super(source);
		mMessageID = source.getMessageID();
	}

	@Override
    protected void updateMessageID() throws MessagingException {
    	setHeader("Message-ID", mMessageID);
    }
}

class ItemData {
	public long updateTime;
	public boolean completed;
	public boolean locked;
	public boolean tomorrow;
	public ItemColor color;
	public String text;
	public String id;

	public ItemData(MimeMessage msg) throws MessagingException {
		updateTime = msg.getSentDate().getTime();
		Flags flags = msg.getFlags();
		completed = flags.contains(Flags.Flag.ANSWERED);
		locked = flags.contains("locked");
		tomorrow = flags.contains("tomorrow");
		color = ItemColor.NONE;
		String[] userFlags = flags.getUserFlags();
		for (String userflag : userFlags) {
	        color = ItemColor.fromKey(userflag, ItemColor.NONE);
			if (ItemColor.NONE != color) break;
		}
		id = msg.getMessageID();
		text = msg.getSubject();    	
	}	
}




public class IMAPSync extends AsyncTask<Void, Void, Void> {
	
	private AppModel mModel;
	private MainActivityState mState;
	private String mIMAPServer;
	private String mIMAPUsername;
	private String mIMAPPassword;

	 /**
     * Constructor 
     * 
     */
    public IMAPSync(MainActivityState mainActivityState) {
    	mState = mainActivityState;
    	mModel = mainActivityState.model();
    	mIMAPServer = mState.prefReader().getIMAPSyncServer();
    	mIMAPUsername = mState.prefReader().getIMAPSyncUsername(); 
    	mIMAPPassword = mState.prefReader().getIMAPSyncPwd();
    	if (0 == mIMAPPassword.length()) {
        	// TODO Ask for password like in http://examples.javacodegeeks.com/android/core/ui/alertdialog/android-prompt-user-input-dialog-example/    		
    	}
    }

	 /**
     * doInBackground 
     * 
     */
    @Override
    protected Void doInBackground(Void... params) {
    	// TODO change the model only if all read/write operations were ok
    	Properties props = System.getProperties();
    	props.setProperty("mail.store.protocol", "imaps");
    	Session session = Session.getInstance(props, null);
    	try {
        	long syncTime = System.currentTimeMillis();
	    	Store store = session.getStore();
			store.connect(mIMAPServer, mIMAPUsername, mIMAPPassword);
			Folder inbox = store.getFolder("INBOX");
			inbox.open(Folder.READ_WRITE);
			
			Log.d("IMAP", "SendNewItems: today="+mModel.getPageItemCount(PageKind.TODAY));
			SendNewItems(PageKind.TODAY, session, inbox);
			SendNewItems(PageKind.TOMOROW, session, inbox);
			
			Log.d("IMAP", "SendRemovedItems: today="+mModel.getPageItemCount(PageKind.TODAY));
			SendRemovedItems(inbox);
			
			Log.d("IMAP", "ReceiveNewItems: today="+mModel.getPageItemCount(PageKind.TODAY));
			ReceiveNewItems(inbox);
			
			Log.d("IMAP", "ReceiveRemovedItems: today="+mModel.getPageItemCount(PageKind.TODAY));
			ReceiveRemovedItems(PageKind.TODAY,  inbox);
			ReceiveRemovedItems(PageKind.TOMOROW, inbox);
			
			Log.d("IMAP", "SynchronizeItemProperties: today="+mModel.getPageItemCount(PageKind.TODAY));
			SynchronizeItemProperties(inbox);

			Log.d("IMAP", "Closing today="+mModel.getPageItemCount(PageKind.TODAY));
			inbox.close(false);
			mModel.setLastSync(syncTime);
			mModel.setDirty();
		} catch (NoSuchProviderException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (MessagingException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
    	return null;
    }
    
    @Override
    protected void onPostExecute(Void result) {
    	// TODO only when everything ok
    	mState.view().updatePages();    	
    }

    /**
     * Write all values of an item (text, color, ...) to an IMAP message.
     * If the message is new, you still need to add it to a Folder using appendMessages().
     */
    protected void WriteItemToMessage(PageKind sourcePageKind, ItemModel sourceItem, MimeMessage targetMsg, boolean isNew) throws MessagingException, IOException {
    	targetMsg.setSentDate(new Date(sourceItem.getUpdateTime()));
    	targetMsg.setSubject(sourceItem.getText());
       	targetMsg.setFlags(new Flags("tomorrow"), (PageKind.TOMOROW == sourcePageKind));
		targetMsg.setFlag(Flag.ANSWERED, sourceItem.isCompleted());					
		targetMsg.setFlags(new Flags("locked"), sourceItem.isLocked());
		for(ItemColor color: ItemColor.values()) {
			targetMsg.setFlags(new Flags(color.getKey()), false);							
		}
		if (ItemColor.NONE != sourceItem.getColor()) {
			targetMsg.setFlags(new Flags(sourceItem.getColor().getKey()), true);					
		}
		if (isNew) {	
			targetMsg.setContent("", "text/plain");    	
		}
    }
    
    /**
     * Send to server all the items which have been created locally
     * @throws MessagingException 
     * @throws IOException 
     */
    protected void SendNewItems(PageKind pageKind, Session session, Folder mailFolder) throws MessagingException, IOException {
    	int iEnd = mModel.getPageItemCount(pageKind);
		for(int i=0; i < iEnd; i++) {
			ItemModel item = mModel.getItemForMutation(pageKind, i);
			if (item.isNeverSynced()) {
				Log.d("IMAP", "Neversync: "+i+" "+item.getText());
				ManianaMessage msg = new ManianaMessage(session, item.getId());
				WriteItemToMessage(pageKind, item, msg, true);
				mailFolder.appendMessages(new Message[] {msg});
				item.setNeverSynced(false);
			}
		}
    }
    
    /**
     * Remove from server all the items which have been removed locally
     * @throws MessagingException 
     */
    protected void SendRemovedItems(Folder mailFolder) throws MessagingException {
    	final IDSetModel mRemovedIDs = mModel.getIDsToRemoveFromServer();
		Log.d("IMAP", "mRemovedIDs.size = "+mRemovedIDs.size());
    	if (!mRemovedIDs.isEmpty()) {
    		int iEnd = mailFolder.getMessageCount();
    		for(int i=1; i <= iEnd; i++) {
    			MimeMessage msg = (MimeMessage) mailFolder.getMessage(i);
				if (msg.getFlags().contains(Flags.Flag.DELETED)) continue;
    			if (mRemovedIDs.contains(msg.getMessageID())) {
    				Log.d("IMAP", "Removing msg "+msg.getSubject());
    				msg.setFlag(Flags.Flag.DELETED, true);					
    			}
    		}
    		mailFolder.expunge();
        	mRemovedIDs.clear();
    	}
    }
    

    /**
     * Get from server all new items that have been creates remotely:
     * As local changes are synchronized first, I just need to create locally all messages that exists only on the server.   
     * @throws MessagingException 
     */
    protected void ReceiveNewItems(Folder mailFolder) throws MessagingException {
		IDSetModel messageIDsLocally = new IDSetModel();
		messageIDsLocally.addIDsFromPage(mModel, PageKind.TODAY);
		messageIDsLocally.addIDsFromPage(mModel, PageKind.TOMOROW);
		int iEnd = mailFolder.getMessageCount();
		Log.d("IMAP", "Msg on server = "+iEnd+" ("+mailFolder.getMessageCount()+" "+mailFolder.getDeletedMessageCount()+")");
		for(int i=1; i <= iEnd; i++) {
			MimeMessage msg = (MimeMessage) mailFolder.getMessage(i);
			if (msg.getFlags().contains(Flags.Flag.DELETED)) continue;
			if (!messageIDsLocally.contains(msg.getMessageID())) {
				appendItemFromItemData(new ItemData(msg));
			}
		}
    }
    
    protected void appendItemFromItemData(ItemData data) throws MessagingException {   	
		Log.d("IMAP", "New msg "+data.text);
		ItemModel item = new ItemModel(data.updateTime, data.id, data.text, data.completed, data.locked, data.color);
		item.setNeverSynced(false);
		PageKind newPageKind = (data.tomorrow)?PageKind.TOMOROW:PageKind.TODAY;
        if (mState.prefTracker().getAddToTopPreference()) {
            mModel.insertItem(newPageKind, 0, item);
        } else {
			mModel.appendItem(newPageKind, item);        	
        }
    }
   
    /**
     * Get from Server all items that have been removed remotely removed:
     * As local changes are synchronized first, I just need to remove locally all messages that don't exist on the server.
     * @throws MessagingException 
     */
    protected void ReceiveRemovedItems(PageKind pageKind, Folder mailFolder) throws MessagingException {
    	// TODO Do not read the HashSet for each page!
    	IDSetModel messageIDsOnServer = new IDSetModel();
    	int iEnd = mailFolder.getMessageCount();
		Log.d("IMAP", "Msg on server = "+iEnd+" ("+mailFolder.getMessageCount()+"  "+mailFolder.getDeletedMessageCount()+")");
		for(int i=1; i <= iEnd; i++) {
			MimeMessage msg = (MimeMessage) mailFolder.getMessage(i);
			if (msg.getFlags().contains(Flags.Flag.DELETED)) continue;
			Log.d("IMAP", " - "+MsgToStr(msg));
			messageIDsOnServer.add(msg.getMessageID());				
		}
		Log.d("IMAP", "Msg on server counted = "+messageIDsOnServer.size());

    	for(int i=mModel.getPageItemCount(pageKind)-1; i >= 0; i--) {
			ItemModel item = mModel.getItemForMutation(pageKind, i);
			if (!messageIDsOnServer.contains(item.getId())) {
				Log.d("IMAP", "Removing "+item.getText());
				mModel.removeItem(pageKind, i);
			}
		}
    }
    
    /**
     * Useful for debugging...
     * @throws MessagingException 
     */
    protected String MsgToStr(MimeMessage msg) throws MessagingException {
    	String result = msg.getSubject();
    	result += " " + msg.getSentDate() + " " + msg.getReceivedDate();
    	for(Flag flag: msg.getFlags().getSystemFlags()) {
    		result += " " + flag.toString();
    	}
    	for(String flag: msg.getFlags().getUserFlags()) {
    		result += " " + flag;
    	}
    	return result;
    }

    /**
     * Last step after synchronizing all new and remove operation:
     * synchronize the properties using the time to check for changes and choose which version to keep.
     * @throws MessagingException 
     * @throws IOException 
     */
    protected void SynchronizeItemProperties(Folder mailFolder) throws MessagingException, IOException {
    	boolean needExpunge = false;
    	int iEnd = mailFolder.getMessageCount();
		for(int i=1; i <= iEnd; i++) {
			MimeMessage msg = (MimeMessage) mailFolder.getMessage(i);
			if (msg.getFlags().contains(Flags.Flag.DELETED)) continue;
			if (SynchronizeItemProperties(msg, mailFolder)) {
				needExpunge = true;
			}
		}
		if (needExpunge) {
			mailFolder.expunge();			
		}
    }

    /**
     * 
     * @return true if the mailbox will need a call to expunge
     * @throws MessagingException
     * @throws IOException
     */
    protected boolean SynchronizeItemProperties(MimeMessage msg, Folder mailFolder) throws MessagingException, IOException {
		ItemData data = new ItemData(msg);
		Pair<PageKind, Integer> itemPageAndIndex = mModel.findItemPageAndIndex(data.id);
		ItemModel item = mModel.getItemForMutation(itemPageAndIndex.first, itemPageAndIndex.second);
		int compareResult = CompareTime(data.updateTime, item.getUpdateTime());
		if (compareResult < 0) {	
    		// Most recent version = local
			Log.d("IMAP", "Updating remote "+item.getText()+" servertime: "+data.updateTime+" localtime:"+item.getUpdateTime());
			ManianaMessage newMsg = new ManianaMessage(msg);
    		WriteItemToMessage(itemPageAndIndex.first, item, newMsg, false);
			mailFolder.appendMessages(new Message[] {newMsg});
			msg.setFlag(Flags.Flag.DELETED, true);
			return true;
    	} else {		
			Log.d("IMAP", "Updating local "+item.getText()+" servertime: "+data.updateTime+" localtime:"+item.getUpdateTime());
    		// Most recent version = on the server (Or no changes, but we can't know for sure as Emails clients wouldn't change the time of the message)
    		boolean localTomorrow = (itemPageAndIndex.first == PageKind.TOMOROW);
    		if (data.tomorrow != localTomorrow) {
    			mModel.removeItem(itemPageAndIndex.first, itemPageAndIndex.second);
    			appendItemFromItemData(data);
    		} else {
    			item.setText(data.text);
    			item.setIsCompleted(data.completed);
    			item.setIsLocked(data.locked);
    			item.setColor(data.color);
    			item.setUpdateTime(data.updateTime);
    		}
    		return false;
    	}
    }
    
    /**
     * Compare an IMAP and a local time ignoring small differences, as the IMAP time format doesn't have milliseconds.
     */
    protected int CompareTime(long time1, long time2) {
    	long time1_seconds = time1 / 1000;
    	long time2_seconds = time2 / 1000;
    	if (time1_seconds < time2_seconds) {
    		return -1;
    	} else if (time1_seconds > time2_seconds) {
    		return 1;
    	} else {
    		return 0;
    	}
    }
}
